#include "Shader.hpp"
#include <iostream>
#include <fstream>

static void printProgramInfoLog(GLuint obj)
{
	int infologLength = 0;
	int charsWritten  = 0;
	char *infoLog;

	glGetProgramiv(obj, GL_INFO_LOG_LENGTH, &infologLength);

	if(infologLength > 0)
	{
		infoLog = (char *)malloc(infologLength);
		glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog);
		printf("%s\n", infoLog);
		free(infoLog);
	}
}

namespace gl
{

Shader *Shader::current = nullptr;

Shader::Shader(bool createdSuccessfully, GLuint shaderProgram, GLuint vertexShader, GLuint fragmentShader) :
	mCreatedSuccessfully(createdSuccessfully),
	mShaderProgram(shaderProgram),
	mVertexShader(vertexShader),
	mFragmentShader(fragmentShader)
{
	if(mCreatedSuccessfully)
	{
		//glUniformBlockBinding(shaderProgram, glGetUniformBlockIndex(shaderProgram, "Matrices"), 1);
		//glUniformBlockBinding(shaderProgram, glGetUniformBlockIndex(shaderProgram, "Material"), 2);
		//mTexUnit = glGetUniformLocation(mShaderProgram, "texUnit");
		//printf("Created shader successfully!\n");
		GLint positionAttrib = glGetAttribLocation(mShaderProgram, "position");
		GLint normalAttrib = glGetAttribLocation(mShaderProgram, "normal");
		GLint texCoordAttrib = glGetAttribLocation(mShaderProgram, "texCoord");
		debugstring("[Shader]: position(%d), normal(%d), texCoord(%d)\n", positionAttrib, normalAttrib, texCoordAttrib);
	}
}

Shader Shader::loadFromFile(const std::string &filepath, Shader::Type shaderType)
{
	std::ifstream file(filepath.c_str());
	if(file) // If file exists
	{
		file.seekg(0, file.end);
		size_t filesize = file.tellg();
		file.seekg(0, file.beg);

		char *filebuffer = new char[filesize + 1];
		file.read(filebuffer, filesize);
		filebuffer[filesize] = NULL;
		file.close();

		switch(shaderType)
		{
			case Type::VERTEX:
			{
				GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
				glShaderSource(vertexShader, 1, (const GLchar**)&filebuffer, NULL);
				glCompileShader(vertexShader);
				int vertexShaderCompileStatus = -1;
				glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
				if(vertexShaderCompileStatus != GL_TRUE)
				{
					#ifdef PHX_DEBUG
					std::cerr << "Vertex shader compilation failed with this message:" << std::endl;
					const int logLength = 512;
					char log[logLength];
					glGetShaderInfoLog(vertexShader, logLength, NULL, log);
					std::cerr << log << std::endl;
					#endif
					delete []filebuffer;
					glDeleteShader(vertexShader);
					return Shader(false);
				}
				delete []filebuffer;

				GLuint shaderProgram = glCreateProgram();
				if(shaderProgram == 0)
				{
					glDeleteShader(vertexShader);
					return Shader(false);
				}

				glAttachShader(shaderProgram, vertexShader);

				glBindFragDataLocation(shaderProgram, 0, "outputColor");

				//glBindAttribLocation(shaderProgram, 0, "position");
				//glBindAttribLocation(shaderProgram, 1, "normal");
				//glBindAttribLocation(shaderProgram, 2, "texCoord");

				glLinkProgram(shaderProgram);
				glValidateProgram(shaderProgram);
				printProgramInfoLog(shaderProgram);

				//Check for errors
				GLint programSuccess = GL_TRUE;
				glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
				if(programSuccess != GL_TRUE)
				{
					#ifdef PHX_DEBUG
					std::cerr << "Failed to link shaders, failed with error:" << std::endl;
					const int logLength = 512;
					char log[logLength];
					glGetShaderInfoLog(shaderProgram, logLength, NULL, log);
					std::cerr << log << std::endl;
					#endif
					glDeleteShader(vertexShader);
					glDeleteProgram(shaderProgram);
					return Shader(false);
				}
				return Shader(true, shaderProgram, vertexShader);
			}
			case Type::FRAGMENT:
			{
				GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
				glShaderSource(fragmentShader, 1, (const GLchar**)&filebuffer, NULL);
				glCompileShader(fragmentShader);
				int fragmentShaderCompileStatus = -1;
				glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
				if(fragmentShaderCompileStatus != GL_TRUE)
				{
					#ifdef PHX_DEBUG
					std::cerr << "Fragment shader compilation failed with this message:" << std::endl;
					const int logLength = 512;
					char log[logLength];
					glGetShaderInfoLog(fragmentShader, logLength, NULL, log);
					std::cerr << log << std::endl;
					#endif
					delete []filebuffer;
					glDeleteShader(fragmentShader);
					return Shader(false);
				}
				delete []filebuffer;

				GLuint shaderProgram = glCreateProgram();
				if(shaderProgram == 0)
				{
					glDeleteShader(fragmentShader);
					return Shader(false);
				}

				glAttachShader(shaderProgram, fragmentShader);

				glBindFragDataLocation(shaderProgram, 0, "outputColor");

				//glBindAttribLocation(shaderProgram, 0, "position");
				//glBindAttribLocation(shaderProgram, 1, "normal");
				//glBindAttribLocation(shaderProgram, 2, "texCoord");

				glLinkProgram(shaderProgram);
				glValidateProgram(shaderProgram);
				printProgramInfoLog(shaderProgram);

				//Check for errors
				GLint programSuccess = GL_TRUE;
				glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
				if(programSuccess != GL_TRUE)
				{
					#ifdef PHX_DEBUG
					std::cerr << "Failed to link shaders, failed with error:" << std::endl;
					const int logLength = 512;
					char log[logLength];
					glGetShaderInfoLog(shaderProgram, logLength, NULL, log);
					std::cerr << log << std::endl;
					#endif
					glDeleteShader(fragmentShader);
					glDeleteProgram(shaderProgram);
					return Shader(false);
				}
				return Shader(true, shaderProgram, 0, fragmentShader);
			}
		}
	}
	return Shader(false);
}

Shader Shader::loadFromFile(const std::string &vertexFilepath, const std::string &fragmentFilepath)
{ 
	char *vertexFileBuffer = NULL;
	char *fragmentFileBuffer = NULL;

	std::ifstream vertexFile(vertexFilepath.c_str());
	if(vertexFile) // If file exists
	{
		vertexFile.seekg(0, vertexFile.end);
		size_t filesize = vertexFile.tellg();
		vertexFile.seekg(0, vertexFile.beg);

		vertexFileBuffer = new char[filesize + 1];
		vertexFile.read(vertexFileBuffer, filesize);
		vertexFileBuffer[filesize] = NULL;
		vertexFile.close();
	}
	else
		return Shader(false);

	std::ifstream fragmentFile(fragmentFilepath.c_str());
	if(fragmentFile) // If file exists
	{
		fragmentFile.seekg(0, fragmentFile.end);
		size_t filesize = fragmentFile.tellg();
		fragmentFile.seekg(0, fragmentFile.beg);

		fragmentFileBuffer = new char[filesize + 1];
		fragmentFile.read(fragmentFileBuffer, filesize);
		fragmentFileBuffer[filesize] = NULL;
		fragmentFile.close();
	}
	else
	{
		delete []vertexFileBuffer;
		return Shader(false);
	}

	GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, (const GLchar**)&vertexFileBuffer, NULL);
	glCompileShader(vertexShader);
	int vertexShaderCompileStatus = -1;
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
	if(vertexShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Vertex shader compilation failed with this message:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		glGetShaderInfoLog(vertexShader, logLength, NULL, log);
		std::cerr << log << std::endl;
		#endif
		delete []vertexFileBuffer;
		delete []fragmentFileBuffer;
		glDeleteShader(vertexShader);
		return Shader(false);
	}

	GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, (const GLchar**)&fragmentFileBuffer, NULL);
	glCompileShader(fragmentShader);
	int fragmentShaderCompileStatus = -1;
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
	if(fragmentShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Fragment shader compilation failed with this message:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		glGetShaderInfoLog(fragmentShader, logLength, NULL, log);
		std::cerr << log << std::endl;
		#endif
		delete []vertexFileBuffer;
		delete []fragmentFileBuffer;
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	delete []vertexFileBuffer;
	delete []fragmentFileBuffer;

	GLuint shaderProgram = glCreateProgram();
	if(shaderProgram == 0)
	{
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);

	glBindFragDataLocation(shaderProgram, 0, "outputColor");

	//glBindAttribLocation(shaderProgram, 0, "position");
	//glBindAttribLocation(shaderProgram, 1, "normal");
	//glBindAttribLocation(shaderProgram, 2, "texCoord");

	glLinkProgram(shaderProgram);
	glValidateProgram(shaderProgram);
	printProgramInfoLog(shaderProgram);

	//Check for errors
	GLint programSuccess = GL_TRUE;
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
	if(programSuccess != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Failed to link shaders, failed with error:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		glGetShaderInfoLog(shaderProgram, logLength, NULL, log);
		std::cerr << log << std::endl;
		#endif
		glDeleteProgram(shaderProgram);
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	return Shader(true, shaderProgram, vertexShader, fragmentShader);
}

Shader Shader::loadFromMemory(const char *shaderSource, Shader::Type shaderType)
{
	switch(shaderType)
	{
		case Type::VERTEX:
		{
			GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
			glShaderSource(vertexShader, 1, (const GLchar**)&shaderSource, NULL);
			glCompileShader(vertexShader);
			int vertexShaderCompileStatus = -1;
			glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
			if(vertexShaderCompileStatus != GL_TRUE)
			{
				#ifdef PHX_DEBUG
				std::cerr << "Vertex shader compilation failed with this message:" << std::endl;
				const int logLength = 512;
				char log[logLength];
				glGetShaderInfoLog(vertexShader, logLength, NULL, log);
				std::cerr << log << std::endl;
				#endif
				glDeleteShader(vertexShader);
				return Shader(false);
			}

			GLuint shaderProgram = glCreateProgram();
			if(shaderProgram == 0)
			{
				glDeleteShader(vertexShader);
				return Shader(false);
			}

			glAttachShader(shaderProgram, vertexShader);

			glBindFragDataLocation(shaderProgram, 0, "outputColor");

			//glBindAttribLocation(shaderProgram, 0, "position");
			//glBindAttribLocation(shaderProgram, 1, "normal");
			//glBindAttribLocation(shaderProgram, 2, "texCoord");

			glLinkProgram(shaderProgram);
			glValidateProgram(shaderProgram);
			printProgramInfoLog(shaderProgram);

			//Check for errors
			GLint programSuccess = GL_TRUE;
			glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
			if(programSuccess != GL_TRUE)
			{
				#ifdef PHX_DEBUG
				std::cerr << "Failed to link shaders, failed with error:" << std::endl;
				const int logLength = 512;
				char log[logLength];
				glGetShaderInfoLog(shaderProgram, logLength, NULL, log);
				std::cerr << log << std::endl;
				#endif
				glDeleteShader(vertexShader);
				glDeleteProgram(shaderProgram);
				return Shader(false);
			}
			return Shader(true, shaderProgram, vertexShader);
		}
		case Type::FRAGMENT:
		{
			GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
			glShaderSource(fragmentShader, 1, (const GLchar**)&shaderSource, NULL);
			glCompileShader(fragmentShader);
			int fragmentShaderCompileStatus = -1;
			glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
			if(fragmentShaderCompileStatus != GL_TRUE)
			{
				#ifdef PHX_DEBUG
				std::cerr << "Fragment shader compilation failed with this message:" << std::endl;
				const int logLength = 512;
				char log[logLength];
				glGetShaderInfoLog(fragmentShader, logLength, NULL, log);
				std::cerr << log << std::endl;
				#endif
				glDeleteShader(fragmentShader);
				return Shader(false);
			}

			GLuint shaderProgram = glCreateProgram();
			if(shaderProgram == 0)
			{
				glDeleteShader(fragmentShader);
				return Shader(false);
			}

			glAttachShader(shaderProgram, fragmentShader);

			glBindFragDataLocation(shaderProgram, 0, "outputColor");

			//glBindAttribLocation(shaderProgram, 0, "position");
			//glBindAttribLocation(shaderProgram, 1, "normal");
			//glBindAttribLocation(shaderProgram, 2, "texCoord");

			glLinkProgram(shaderProgram);
			glValidateProgram(shaderProgram);
			printProgramInfoLog(shaderProgram);

			//Check for errors
			GLint programSuccess = GL_TRUE;
			glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
			if(programSuccess != GL_TRUE)
			{
				#ifdef PHX_DEBUG
				std::cerr << "Failed to link shaders, failed with error:" << std::endl;
				const int logLength = 512;
				char log[logLength];
				glGetShaderInfoLog(shaderProgram, logLength, NULL, log);
				std::cerr << log << std::endl;
				#endif
				glDeleteShader(fragmentShader);
				glDeleteProgram(shaderProgram);
				return Shader(false);
			}
			return Shader(true, shaderProgram, 0, fragmentShader);
		}
	}
	return Shader(false);
}

Shader Shader::loadFromMemory(const char *vertexSource, const char *fragmentSource)
{
	GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexSource, NULL);
	glCompileShader(vertexShader);
	int vertexShaderCompileStatus = -1;
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
	if(vertexShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Vertex shader compilation failed with this message:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		glGetShaderInfoLog(vertexShader, logLength, NULL, log);
		std::cerr << log << std::endl;
		#endif
		glDeleteShader(vertexShader);
		return Shader(false);
	}

	GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
	glCompileShader(fragmentShader);
	int fragmentShaderCompileStatus = -1;
	glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
	if(fragmentShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Fragment shader compilation failed with this message:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		glGetShaderInfoLog(fragmentShader, logLength, NULL, log);
		std::cerr << log << std::endl;
		#endif
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	GLuint shaderProgram = glCreateProgram();
	if(shaderProgram == 0)
	{
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);

	glBindFragDataLocation(shaderProgram, 0, "outputColor");

	//glBindAttribLocation(shaderProgram, 0, "position");
	//glBindAttribLocation(shaderProgram, 1, "normal");
	//glBindAttribLocation(shaderProgram, 2, "texCoord");

	glLinkProgram(shaderProgram);
	glValidateProgram(shaderProgram);
	printProgramInfoLog(shaderProgram);

	//Check for errors
	GLint programSuccess = GL_TRUE;
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &programSuccess);
	if(programSuccess != GL_TRUE)
	{
		#ifdef PHX_DEBUG
		std::cerr << "Failed to link shaders, failed with error:" << std::endl;
		const int logLength = 512;
		char log[logLength];
		GLsizei genLength = 0;
		glGetShaderInfoLog(shaderProgram, logLength, &genLength, log);
		if(genLength == 0)
			std::cerr << "Unknown error (No error message given)." << std::endl;
		else
			std::cerr << log << std::endl;
		#endif
		glDeleteProgram(shaderProgram);
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		return Shader(false);
	}

	return Shader(true, shaderProgram, vertexShader, fragmentShader);
}

bool Shader::use()
{
	if(mCreatedSuccessfully)
	{
		glUseProgram(mShaderProgram);
		current = this;
		return true;
	}
	return false;
}

bool Shader::setValue(const std::string &variable, const glm::mat4 &value)
{
	glUseProgram(mShaderProgram);
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, variable.c_str());
	if(uniformLocation == -1) return false;
	glUniformMatrix4fv(uniformLocation, 1, GL_FALSE, glm::value_ptr(value));
	glUseProgram(NULL);
	return true;
}

bool Shader::setValue(const std::string &variable, const int value)
{
	glUseProgram(mShaderProgram);
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, variable.c_str());
	if(uniformLocation == -1) return false;
	glUniform1i(uniformLocation, value);
	glUseProgram(NULL);
	return true;
}

bool Shader::setMaterialValues(const gl::Material &material)
{
	GLint uniformLocation = 0;
	glUseProgram(mShaderProgram);

	if(!setValue("texCount", material.texCount)) return false;

	glUseProgram(mShaderProgram);
	uniformLocation = glGetUniformLocation(mShaderProgram, "diffuse");
	if(uniformLocation == -1) return false;
	glUniform4f(uniformLocation, material.diffuse[0], material.diffuse[1], material.diffuse[2], material.diffuse[3]);

	uniformLocation = glGetUniformLocation(mShaderProgram, "ambient");
	if(uniformLocation == -1) return false;
	glUniform4f(uniformLocation, material.ambient[0], material.ambient[1], material.ambient[2], material.ambient[3]);

	//uniformLocation = glGetUniformLocation(mShaderProgram, "specular");
	//if(uniformLocation == -1) return false;
	//glUniform4f(uniformLocation, material.specular[0], material.specular[1], material.specular[2], material.specular[3]);
	//printf("Specular set\n");

	//uniformLocation = glGetUniformLocation(mShaderProgram, "emissive");
	//if(uniformLocation == -1) return false;
	//glUniform4f(uniformLocation, material.emissive[0], material.emissive[1], material.emissive[2], material.emissive[3]);
	//printf("Emission set\n");

	//uniformLocation = glGetUniformLocation(mShaderProgram, "shininess");
	//if(uniformLocation == -1) return false;
	//glUniform1f(uniformLocation, material.shininess);
	//printf("Shininess set\n");

	//uniformLocation = glGetUniformLocation(mShaderProgram, "texCount");
	//if(uniformLocation == -1) return false;
	//glUniform1i(uniformLocation, material.texCount);
	//printf("texCount set\n");

	glUseProgram(NULL);
	return true;
}

bool Shader::bindUniformBlock(const std::string &blockName, const gl::UniformBlock &uniformBlock)
{
	GLuint blockIndex = glGetUniformBlockIndex(mShaderProgram, blockName.c_str());
	if(blockIndex == GL_INVALID_INDEX)
		return false;
	glUniformBlockBinding(mShaderProgram, blockIndex, uniformBlock.mBlockLocation);
	return true;
}

Shader::operator bool()
{
	return mCreatedSuccessfully;
}

Shader::~Shader()
{
	glDeleteProgram(mShaderProgram);
	glDeleteShader(mFragmentShader);
	glDeleteShader(mVertexShader);

	if(current == this)
		current = nullptr;
}

}